package org.carabiner.infinitest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

class ExternalProcessTestRunner implements TestRunner
{
  private TestRunnerEventSupport eventSupport;
  private Process process;
  private int currentPort;
  private Class<? extends TestRunner> runnerClass;
    
  public ExternalProcessTestRunner(Class<? extends TestRunner> testRunnerClass) throws IOException
  {
    eventSupport = new TestRunnerEventSupport(this);
    runnerClass = testRunnerClass;
    startProcess();    
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable()
    {
      public void run()
      {
        synchronized (ExternalProcessTestRunner.this)
        {
          process.destroy();  
        }                
      }      
    }));
  }

  synchronized int dispose() throws InterruptedException
  {
    process.destroy();    
    process.waitFor();
    return process.exitValue();
  }
  
  private synchronized void startProcess() throws IOException
  {    
    System.out.print("Spawning new process...");
    String javaExecutable = "java";
    String classpath = System.getProperty("java.class.path");
    String newProcessClassname = TestRunnerProcess.class.getName();
    process = Runtime.getRuntime().exec(javaExecutable + " -cp " + classpath + " " + newProcessClassname + " " + runnerClass.getName());
    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    try
    {
      while(!reader.ready())
        Thread.sleep(20);
    }
    catch (InterruptedException e)
    {
      throw new RuntimeException(e);
    }
    currentPort = Integer.parseInt(reader.readLine());
    System.out.println("...on port " + currentPort);
    reader.close();    
  }

  public void addTestStatusListener(TestStatusListener listener)
  {
    eventSupport.addTestStatusListener(listener);
  }

  public void runTest(String testClass)
  {
    try
    {
      List<TestEvent> result = null;
      result = sendTestToProcess(testClass);  
      if (ranOutOfMemory(result))
      {
        // Kill the test process and try again
        killTest();
        result = sendTestToProcess(testClass);       
      }      
      processEvents((List<TestEvent>) result);
    }
    catch (UnknownHostException e)
    {
      throw new RuntimeException(e);
    }
    catch (SocketException e)
    {
      // Test process was killed, connection was reset.
    }
    catch (IOException e)
    {
      throw new RuntimeException(e);
    }
    catch (ClassNotFoundException e)
    {
      throw new RuntimeException(e);
    }    
    catch (OutOfMemoryError e)
    {
      fireOutOfMemoryEvent(testClass, (Throwable)e);
    }
  }

  private boolean ranOutOfMemory(List<TestEvent> result)
  {
    for (TestEvent event : result)
    {
      if (event.getError() instanceof OutOfMemoryError)
        return true;
    }
    return false;
  }

  private void fireOutOfMemoryEvent(String testClass, Throwable error)
  {    
    fireEvent(createErrorEvent(this, testClass, error));
  }
  
  private static TestEvent createErrorEvent(Object source, String testClass, Throwable error)
  {
    return new TestEvent(source, TestEvent.EventType.TEST_ERROR, error.getMessage(), testClass, "", error);
  }

  private void processEvents(List<TestEvent> eventList)
  {
    for (TestEvent event : eventList)
    {
      fireEvent(event);
    }
  }

  private List<TestEvent> sendTestToProcess(String testClass) throws UnknownHostException, IOException, ClassNotFoundException
  {
    Socket socket = new Socket("127.0.0.1", currentPort);
    try
    {
      ObjectOutputStream ooStream = new ObjectOutputStream(socket.getOutputStream());
      ObjectInputStream oiStream = new ObjectInputStream(socket.getInputStream());
      ooStream.writeObject(testClass);
      List<TestEvent> result = (List<TestEvent>)oiStream.readObject();
      ooStream.close();
      return result;
    }
    finally
    {
      socket.close();
    }
  }
  
  public void runTests(Set<String> testClasses)
  {
    for (String testName : testClasses)
    {
      runTest(testName);
    }
  }
  
  public synchronized void killTest()
  {    
    try
    {
      process.destroy();
      process.waitFor();
      startProcess();
    }
    catch (InterruptedException e)
    {
      throw new RuntimeException(e);
    }
    catch (IOException e)
    {
      throw new RuntimeException(e);
    }
  }

  private void fireEvent(TestEvent event)
  {
    eventSupport.fireTestEvent(event);    
  }
  
  private static class TestRunnerProcess implements TestStatusListener
  {
    private ArrayList<TestEvent> eventList;
    private TestRunner runner;

    public TestRunnerProcess(String runnerClass)
    {      
      runner = createRunner(runnerClass);     
    }
    
    private TestRunner createRunner(String string)
    {
      TestRunner runner = null;
      try
      {
        Class runnerClass = Class.forName(string);
        runner = (TestRunner) runnerClass.newInstance();
      }
      catch (ClassNotFoundException e)
      {
        throw new RuntimeException(e);
      }
      catch (InstantiationException e)
      {
        throw new RuntimeException(e);
      }
      catch (IllegalAccessException e)
      {
        throw new RuntimeException(e);
      }
      runner.addTestStatusListener(this);
      return runner;
    }
   
    public void testCaseSucceeded(TestEvent event)
    {    
      processTestEvent(event);
    }

    public void testError(TestEvent event)
    {
      processTestEvent(event);      
    }

    public void testFailed(TestEvent event)
    {
      processTestEvent(event);      
    }

    public void testMethodSucceeded(TestEvent event)
    {
      processTestEvent(event);      
    }

    private void processTestEvent(TestEvent event)
    {      
      eventList.add(event);                  
    }

    private List<TestEvent> runTest(String testName)
    {
      eventList = new ArrayList<TestEvent>();
      runner.runTest(testName);
      return eventList;
    }
    
    public static void main(String[] args) throws IOException, ClassNotFoundException
    {
      try
      {
        TestRunnerProcess process = new TestRunnerProcess(args[0]);      
        ServerSocket socket = new ServerSocket(0);
        System.out.println(socket.getLocalPort());      
        while (true)
        {
          Socket clientSocket = socket.accept();
          ObjectInputStream oiStream = new ObjectInputStream(clientSocket.getInputStream());
          ObjectOutputStream ooStream = new ObjectOutputStream(clientSocket.getOutputStream());
          String testName = (String) oiStream.readObject();
          try
          {
            List<TestEvent> results = process.runTest(testName);
            ooStream.writeObject(results);
          }
          catch (Throwable e)
          {            
            ooStream.writeObject(createErrorEvent(process, testName, e));
          }
          finally
          {
            clientSocket.close();
          }
        }
      }
      catch (Throwable e)
      {
        System.exit(1);
      }
    }     
  }
}
